package phoenix;

import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.NonNull;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.widget.AbsListView;
import android.widget.ImageView;

import com.stduy.util.Logger;
import com.study.demo.R;

import java.security.InvalidParameterException;

import phoenix.refresh_view.BaseRefreshView;
import phoenix.refresh_view.SunRefreshView;
import phoenix.util.Utils;

public class PullToRefreshView extends ViewGroup {

  private static final int DRAG_MAX_DISTANCE = 120;
  private static final float DRAG_RATE = .5f;
  private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;

  public static final int STYLE_SUN = 0;
  public static final int MAX_OFFSET_ANIMATION_DURATION = 700;

  private static final int INVALID_POINTER = -1;

  private View mTarget;
  private ImageView mRefreshView;
  private Interpolator mDecelerateInterpolator;
  private int mTouchSlop;
  private int mTotalDragDistance;
  private BaseRefreshView mBaseRefreshView;
  private float mCurrentDragPercent;
  private int mCurrentOffsetTop;
  private boolean mRefreshing;
  private int mActivePointerId;
  private boolean mIsBeingDragged;
  private float mInitialMotionY;
  private int mFrom;
  private float mFromDragPercent;
  private boolean mNotify;
  private OnRefreshListener mListener;

  private int mTargetPaddingTop;
  private int mTargetPaddingBottom;
  private int mTargetPaddingRight;
  private int mTargetPaddingLeft;

  public PullToRefreshView(Context context) {
    this(context, null);
  }

  public PullToRefreshView(Context context, AttributeSet attrs) {
    super(context, attrs);
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshView);
    final int type = a.getInteger(R.styleable.RefreshView_type, STYLE_SUN);
    a.recycle();

    mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    mTotalDragDistance = Utils.convertDpToPixel(context, DRAG_MAX_DISTANCE);

    mRefreshView = new ImageView(context);
    Logger.d("width:" + getWidth());
    setRefreshStyle(type);

    addView(mRefreshView);

    setWillNotDraw(false);
    ViewCompat.setChildrenDrawingOrderEnabled(this, true);
  }

  public void setRefreshStyle(int type) {
    setRefreshing(false);
    switch (type) {
      case STYLE_SUN:
        mBaseRefreshView = new SunRefreshView(getContext(), this);
        break;
      default:
        throw new InvalidParameterException("Type does not exist");
    }
    mRefreshView.setImageDrawable(mBaseRefreshView);
  }

  /**
   * This method sets padding for the refresh (progress) view.
   */
  public void setRefreshViewPadding(int left, int top, int right, int bottom) {
    mRefreshView.setPadding(left, top, right, bottom);
  }

  public int getTotalDragDistance() {
    return mTotalDragDistance;
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    ensureTarget();
    if (mTarget == null) return;

    widthMeasureSpec =
        MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingRight() - getPaddingLeft(),
            MeasureSpec.EXACTLY);
    heightMeasureSpec =
        MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
            MeasureSpec.EXACTLY);
    mTarget.measure(widthMeasureSpec, heightMeasureSpec);
    mRefreshView.measure(widthMeasureSpec, heightMeasureSpec);
  }

  private void ensureTarget() {
    if (mTarget != null) {
      return;
    }
    if (getChildCount() > 0) {
      for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        if (child != mRefreshView) {
          mTarget = child;
          mTargetPaddingBottom = mTarget.getPaddingBottom();
          mTargetPaddingLeft = mTarget.getPaddingLeft();
          mTargetPaddingRight = mTarget.getPaddingRight();
          mTargetPaddingTop = mTarget.getPaddingTop();
        }
      }
    }
  }

  @Override
  public boolean onInterceptTouchEvent(MotionEvent ev) {

    if (!isEnabled() || canChildScrollUp() || mRefreshing) {
      return false;
    }

    final int action = MotionEventCompat.getActionMasked(ev);

    switch (action) {
      case MotionEvent.ACTION_DOWN:
        setTargetOffsetTop(0, true);
        mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
        mIsBeingDragged = false;
        final float initialMotionY = getMotionEventY(ev, mActivePointerId);
        if (initialMotionY == -1) {
          return false;
        }
        mInitialMotionY = initialMotionY;
        break;
      case MotionEvent.ACTION_MOVE:
        if (mActivePointerId == INVALID_POINTER) {
          return false;
        }
        final float y = getMotionEventY(ev, mActivePointerId);
        if (y == -1) {
          return false;
        }
        final float yDiff = y - mInitialMotionY;
        if (yDiff > mTouchSlop && !mIsBeingDragged) {
          mIsBeingDragged = true;
        }
        break;
      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_CANCEL:
        mIsBeingDragged = false;
        mActivePointerId = INVALID_POINTER;
        break;
      case MotionEventCompat.ACTION_POINTER_UP:
        onSecondaryPointerUp(ev);
        break;
    }

    return mIsBeingDragged;
  }

  @Override
  public boolean onTouchEvent(@NonNull MotionEvent ev) {

    if (!mIsBeingDragged) {
      return super.onTouchEvent(ev);
    }

    final int action = MotionEventCompat.getActionMasked(ev);

    switch (action) {
      case MotionEvent.ACTION_MOVE: {
        final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
        if (pointerIndex < 0) {
          return false;
        }

        final float y = MotionEventCompat.getY(ev, pointerIndex);
        final float yDiff = y - mInitialMotionY;
        final float scrollTop = yDiff * DRAG_RATE;
        mCurrentDragPercent = scrollTop / mTotalDragDistance;
        if (mCurrentDragPercent < 0) {
          return false;
        }
        float boundedDragPercent = Math.min(1f, Math.abs(mCurrentDragPercent));
        float extraOS = Math.abs(scrollTop) - mTotalDragDistance;
        float slingshotDist = mTotalDragDistance;
        float tensionSlingshotPercent =
            Math.max(0, Math.min(extraOS, slingshotDist * 2) / slingshotDist);
        float tensionPercent =
            (float) ((tensionSlingshotPercent / 4) - Math.pow((tensionSlingshotPercent / 4), 2))
                * 2f;
        float extraMove = (slingshotDist) * tensionPercent / 2;
        int targetY = (int) ((slingshotDist * boundedDragPercent) + extraMove);

        mBaseRefreshView.setPercent(mCurrentDragPercent, true);
        setTargetOffsetTop(targetY - mCurrentOffsetTop, true);
        break;
      }
      case MotionEventCompat.ACTION_POINTER_DOWN:
        final int index = MotionEventCompat.getActionIndex(ev);
        mActivePointerId = MotionEventCompat.getPointerId(ev, index);
        break;
      case MotionEventCompat.ACTION_POINTER_UP:
        onSecondaryPointerUp(ev);
        break;
      case MotionEvent.ACTION_UP:
      case MotionEvent.ACTION_CANCEL: {
        if (mActivePointerId == INVALID_POINTER) {
          return false;
        }
        final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
        final float y = MotionEventCompat.getY(ev, pointerIndex);
        final float overScrollTop = (y - mInitialMotionY) * DRAG_RATE;
        mIsBeingDragged = false;
        if (overScrollTop > mTotalDragDistance) {
          setRefreshing(true, true);
        } else {
          mRefreshing = false;
          animateOffsetToStartPosition();
        }
        mActivePointerId = INVALID_POINTER;
        return false;
      }
    }

    return true;
  }

  private void animateOffsetToStartPosition() {
    mFrom = mCurrentOffsetTop;
    mFromDragPercent = mCurrentDragPercent;
    long animationDuration = Math.abs((long) (MAX_OFFSET_ANIMATION_DURATION * mFromDragPercent));

    mAnimateToStartPosition.reset();
    mAnimateToStartPosition.setDuration(animationDuration);
    mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
    mAnimateToStartPosition.setAnimationListener(mToStartListener);
    mRefreshView.clearAnimation();
    mRefreshView.startAnimation(mAnimateToStartPosition);
  }

  private void animateOffsetToCorrectPosition() {
    mFrom = mCurrentOffsetTop;
    mFromDragPercent = mCurrentDragPercent;

    mAnimateToCorrectPosition.reset();
    mAnimateToCorrectPosition.setDuration(MAX_OFFSET_ANIMATION_DURATION);
    mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator);
    mRefreshView.clearAnimation();
    mRefreshView.startAnimation(mAnimateToCorrectPosition);

    if (mRefreshing) {
      mBaseRefreshView.start();
      if (mNotify) {
        if (mListener != null) {
          mListener.onRefresh();
        }
      }
    } else {
      mBaseRefreshView.stop();
      animateOffsetToStartPosition();
    }
    mCurrentOffsetTop = mTarget.getTop();
    mTarget.setPadding(mTargetPaddingLeft, mTargetPaddingTop, mTargetPaddingRight,
        mTotalDragDistance);
  }

  private final Animation mAnimateToStartPosition = new Animation() {
    @Override
    public void applyTransformation(float interpolatedTime, Transformation t) {
      moveToStart(interpolatedTime);
    }
  };

  private final Animation mAnimateToCorrectPosition = new Animation() {
    @Override
    public void applyTransformation(float interpolatedTime, Transformation t) {
      int targetTop;
      int endTarget = mTotalDragDistance;
      targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime));
      int offset = targetTop - mTarget.getTop();

      mCurrentDragPercent = mFromDragPercent - (mFromDragPercent - 1.0f) * interpolatedTime;
      mBaseRefreshView.setPercent(mCurrentDragPercent, false);

      setTargetOffsetTop(offset, false /* requires update */);
    }
  };

  private void moveToStart(float interpolatedTime) {
    int targetTop = mFrom - (int) (mFrom * interpolatedTime);
    float targetPercent = mFromDragPercent * (1.0f - interpolatedTime);
    int offset = targetTop - mTarget.getTop();

    mCurrentDragPercent = targetPercent;
    mBaseRefreshView.setPercent(mCurrentDragPercent, true);
    mTarget.setPadding(mTargetPaddingLeft, mTargetPaddingTop, mTargetPaddingRight,
        mTargetPaddingBottom + targetTop);
    setTargetOffsetTop(offset, false);
  }

  public void setRefreshing(boolean refreshing) {
    if (mRefreshing != refreshing) {
      setRefreshing(refreshing, false /* notify */);
    }
  }

  private void setRefreshing(boolean refreshing, final boolean notify) {
    if (mRefreshing != refreshing) {
      mNotify = notify;
      ensureTarget();
      mRefreshing = refreshing;
      if (mRefreshing) {
        mBaseRefreshView.setPercent(1f, true);
        animateOffsetToCorrectPosition();
      } else {
        animateOffsetToStartPosition();
      }
    }
  }

  private Animation.AnimationListener mToStartListener = new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {
    }

    @Override
    public void onAnimationRepeat(Animation animation) {
    }

    @Override
    public void onAnimationEnd(Animation animation) {
      mBaseRefreshView.stop();
      mCurrentOffsetTop = mTarget.getTop();
    }
  };

  private void onSecondaryPointerUp(MotionEvent ev) {
    final int pointerIndex = MotionEventCompat.getActionIndex(ev);
    final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
    if (pointerId == mActivePointerId) {
      final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
      mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
    }
  }

  private float getMotionEventY(MotionEvent ev, int activePointerId) {
    final int index = MotionEventCompat.findPointerIndex(ev, activePointerId);
    if (index < 0) {
      return -1;
    }
    return MotionEventCompat.getY(ev, index);
  }

  private void setTargetOffsetTop(int offset, boolean requiresUpdate) {
    mTarget.offsetTopAndBottom(offset);
    mBaseRefreshView.offsetTopAndBottom(offset);
    mCurrentOffsetTop = mTarget.getTop();
    if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) {
      invalidate();
    }
  }

  private boolean canChildScrollUp() {
    if (android.os.Build.VERSION.SDK_INT < 14) {
      if (mTarget instanceof AbsListView) {
        final AbsListView absListView = (AbsListView) mTarget;
        return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0
            || absListView.getChildAt(0).getTop() < absListView.getPaddingTop());
      } else {
        return mTarget.getScrollY() > 0;
      }
    } else {
      return ViewCompat.canScrollVertically(mTarget, -1);
    }
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mBaseRefreshView != null) {
      if (!((SunRefreshView) mBaseRefreshView).isCreateBitmap()) {
        ((SunRefreshView) mBaseRefreshView).initiateDimens(getWidth());
      }
    }
    ensureTarget();
    if (mTarget == null) return;
    int height = getMeasuredHeight();
    int width = getMeasuredWidth();
    int left = getPaddingLeft();
    int top = getPaddingTop();
    int right = getPaddingRight();
    int bottom = getPaddingBottom();

    mTarget.layout(left, top + mCurrentOffsetTop, left + width - right,
        top + height - bottom + mCurrentOffsetTop);
    mRefreshView.layout(left, top, left + width - right, top + height - bottom);
  }

  public void setOnRefreshListener(OnRefreshListener listener) {
    mListener = listener;
  }

  public interface OnRefreshListener {
    void onRefresh();
  }
}

